feat(chat): OAuth 2.1 pivot + auth_request UX (PR-B、stacked on #19)#21
Conversation
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughMCPサーバーのPAT/Basic認証設定を削除し、OAuth 2.1フロー検出・処理を追加。認証リクエストを表すチャットブロック/イベントとフロントエンドのUI・送信経路を導入し、SDK向けのMCP設定はURLのみを渡すように変更しました。 Changes
Sequence Diagram(s)sequenceDiagram
participant User as フロントエンドユーザ
participant Front as Frontend UI
participant WS as Frontend WS / ChatHandle
participant Backend as Backend (chat-runner)
participant SDK as SDK/Agent
participant MCP as 外部MCPサーバ
User->>Front: メッセージ送信(OAuth開始)
Front->>WS: メッセージ送信
WS->>Backend: stream/query to SDK
Backend->>SDK: SDKにクエリ(MCP URLのみ)
SDK->>MCP: MCPへ問い合わせ(認可フロー開始)
MCP->>SDK: tool_use: authenticate (authUrl)
SDK->>Backend: stream tool_use
Backend->>Backend: auth-detectorで解析、chat_auth_request(pending)発行
Backend->>WS: chat_auth_request イベント
WS->>Front: イベント配信
Front->>User: AuthRequestCard表示(authUrlリンク + コールバック入力)
User->>Front: ローカルコールバックURLを貼付・送信
Front->>WS: oauth_callback frame (mcpServerId, callbackUrl)
WS->>Backend: oauth_callback受信
Backend->>SDK: runOAuthCallback -> complete_authentication with callback
SDK->>MCP: コールバックをMCPに転送
MCP->>SDK: tool_result (success/failure)
SDK->>Backend: stream tool_result
Backend->>Backend: pending auth_request を completed/failed に更新、chat_auth_request 発行
Backend->>WS: 更新イベント送信
WS->>Front: ブロック更新を反映
Front->>User: 完了/失敗表示(failureMessage表示)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 46 minutes and 40 seconds.Comment |
Premise 9 撤回: PAT only → MCP プロトコルの OAuth 2.1 採用 (user sovereignty)。 理由: - Atlassian 公式 Rovo MCP は OAuth 2.1 ネイティブ、Claude Agent SDK は MCP の 'needs-auth' status を扱う built-in support を持つ → SDK 任せで auto auth flow が回る - sooperset/mcp-atlassian を使う場合も PAT は MCP server 自身の env で持つ前提に - Tally プロセスから PAT/API key の概念が完全消滅 (project.yaml / メモリ / ログのいずれにも残らない) 実装変更: - core: McpServerConfigSchema から auth フィールド削除、関連 hardening (envVar regex / Basic+Bearer 分岐) も削除。schema test を auth-less に整理 - ai-engine: buildMcpServers の auth header 構築ロジック削除、url のみで HTTP config (req は SDK が必要に応じて WWW-Authenticate を解釈)。chat-runner / agent-runner も auth に依存しない。env 未設定 throw test は仕様変更のため削除 - frontend: 設定 dialog から auth scheme dropdown / envVar 入力欄を削除、URL のみのシンプル UI、caption も「OAuth 2.1 / API token は MCP 任せ」に書き換え
外部 MCP の OAuth 認証フロー (authenticate / complete_authentication tool_use) を assistant 文中の生 tool_use ではなく、専用カードで扱う。「Atlassian で認証」 ボタン (新規タブ) と callback URL paste 入力欄を統合。 - packages/core/src/schema.ts: ChatBlock に \`auth_request\` variant を追加 (mcpServerId / mcpServerLabel / authUrl / status / failureMessage)。 status と failureMessage の整合を superRefine で固定: failed なのに message 無し / pending・completed に message が付いているケースを reject。 - packages/ai-engine/src/auth-detector.ts (新規): \`mcp__<id>__authenticate\` / \`complete_authentication\` パターンの分解、tool_result.output からの auth URL 抽出。文末の句読点・括弧を URL に含めない処理 (`...state=xyz.` のような 自然文出力で認可リンクが壊れるのを防ぐ)。 - packages/ai-engine/src/stream.ts: \`chat_auth_request\` event を ChatEvent に追加。 - packages/ai-engine/src/chat-runner.ts: tool_use を stash → tool_result とペアに なった瞬間に auth_request ブロックへ変換 (raw な tool_use/tool_result は出さない)。 \`handleAuthToolResult\` / \`findLatestPendingAuthRequest\` を新設。 complete_authentication 受領時は同 mcpServerId の最新 pending を更新。 - packages/frontend/src/components/chat/auth-request-card.tsx (新規): 認証ボタン (新規タブ) と callback URL paste 入力欄を 1 カードに集約。 callback URL は localhost / 127.0.0.1 のみ受け付ける軽い形式チェック付き。 - packages/frontend/src/components/chat/chat-message.tsx: auth_request → AuthRequestCard dispatch。 - packages/frontend/src/lib/store.ts: \`chat_auth_request\` event 反映 (pending append / completed・failed in-place 更新)。 - core/schema.test.ts: auth_request バリエーション 6 件 (基本 + 状態遷移 reject) - ai-engine/auth-detector.test.ts: parseAuthToolName / extractAuthUrl 計 13 件 (末尾句読点 strip 含む) - ai-engine/chat-runner.test.ts: auth_request 変換フロー 3 件 (authenticate / complete success / complete failure) - frontend/auth-request-card.test.tsx: pending/completed/failed 描画と paste 検証 7 件 注: ChatRunner は per-turn sdk.query() のままで、long-lived Query 化は別 PR (PR-C) で扱う。本 PR の auth_request UX 単体では「auth 1 回 → 同 thread の 次 turn で再 auth が必要」(SDK subprocess が turn ごとに再生成されるため OAuth state が引き継がれない) という挙動を残す。
「mcpServers を空配列で全消去できる」テストの事前 PATCH の status を assert していなかった。失敗していると後続の「空配列で削除」ケース が偽の成功 (空 → 空) で通る恐れがあったため修正。
これまで failed/completed イベントは「同 mcpServerId の pending ブロック を更新」する経路しか持たず、URL 抽出失敗等で pending を append する 前に failed が確定するケースで in-memory state に反映されなかった (YAML には書かれるがリロードまで UI に出ない → 「何も起きない」と見える)。 evt.status==='failed' で同 mcpServerId の pending が見つからない場合、 pending と同様に該当 messageId に新規 append する。completed で pending 不在は異常系のため無視 (現状維持)。 codex セカンドオピニオン (PR #21) Major M1 指摘対応。
…で埋める callback URL paste 後の turn では SDK 応答が complete_authentication tool_use/tool_result のみで、handleAuthToolResult は過去 message の pending ブロックを更新するだけ。結果として現 turn の assistantMsgId には何も append されず、空のアシスタント bubble が thread に蓄積し、 再生時の prompt にもノイズとして入り込む問題があった。 textBuffer が空 + 対象 message のブロックが 0 件なら、プレースホルダ 「(認証処理を完了しました)」テキストを replaceMessageBlocks で書く。 codex セカンドオピニオン (PR #21) Major M2 指摘対応。
… として受付 McpServerConfigSchema.url は loopback として localhost / 127.0.0.1 / ::1 / [::1] を許容しているが、AuthRequestCard 側の isLikelyCallbackUrl は localhost / 127.0.0.1 のみ。IPv6 優先環境で SDK がリダイレクト先を http://[::1]:XXXXX/callback?... で通知すると「認証完了」ボタンが永久 に無効になる可能性があった。schema.ts と loopback 判定を揃える。 codex セカンドオピニオン (PR #21) Minor m1 指摘対応。
9e2679d to
6875149
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
packages/frontend/src/lib/store.ts (1)
385-470: ⚡ Quick win
chat_auth_requestの更新処理は切り出したいです。
store.tsがすでにかなり大きく、ここにイベント分岐を足し続けると追跡とテスト追加がつらいです。applyAuthRequestEventのような helper か別モジュールへ分けると、今回の pending / completed / failed 分岐も独立して検証しやすくなります。As per coding guidelines, "Keep individual files under 500 lines; split larger files into multiple modules".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/lib/store.ts` around lines 385 - 470, The chat_auth_request handling is too large in store.ts—extract it into a dedicated helper (e.g. applyAuthRequestEvent) or a small module that receives (evt, currentMessages) and returns updated messages; move the logic that inspects evt.status ('pending' | 'completed' | 'failed'), searches/updates auth_request blocks by mcpServerId/status, appends failed entries by evt.messageId when no pending exists, and preserves existing block fields (mcpServerLabel, authUrl, failureMessage) into that helper; update the store code to call get().chatThreadMessages, pass them and evt to applyAuthRequestEvent, then call set({ chatThreadMessages: result }) and keep the public behavior identical (use the same symbols: evt, chatThreadMessages, blocks, mcpServerId, status, failureMessage).packages/frontend/src/components/chat/auth-request-card.test.tsx (1)
68-77: ⚡ Quick win
::1許可のポジティブケースを追加してください。Line 68-77 は拒否ケース中心で、IPv6 ループバック (
http://[::1]:...) の許可を明示的に担保できていません。ここは回帰しやすい重要ロジックです。追加テスト例
+ it('host が ::1 の callback URL は許可される', async () => { + const send = vi.fn().mockResolvedValue(undefined); + useCanvasStore.setState({ sendChatMessage: send } as never); + render(<AuthRequestCard block={pendingBlock} />); + const input = screen.getByRole('textbox', { name: /callback URL/ }) as HTMLInputElement; + fireEvent.change(input, { + target: { value: 'http://[::1]:54801/callback?code=AAA&state=xyz' }, + }); + const btn = screen.getByRole('button', { name: /^認証完了$/ }) as HTMLButtonElement; + expect(btn.disabled).toBe(false); + fireEvent.click(btn); + await screen.findByDisplayValue(''); + expect(send).toHaveBeenCalledTimes(1); + });Based on learnings: "Use Vitest for unit testing, and write tests for important logic before merging".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/components/chat/auth-request-card.test.tsx` around lines 68 - 77, Test suite for AuthRequestCard lacks a positive case verifying IPv6 loopback (::1) is accepted; add a unit test in packages/frontend/src/components/chat/auth-request-card.test.tsx that mirrors the existing negative case but sets the input value to an IPv6 loopback callback (e.g. "http://[::1]:3000/callback?code=AAA&state=xyz"), ensure useCanvasStore.setState({ sendChatMessage: vi.fn() } as never) and render(<AuthRequestCard block={pendingBlock} />) as in the other test, then assert the submit button (screen.getByRole('button', { name: /^認証完了$/ })) is enabled (disabled === false) to confirm AuthRequestCard allows ::1 hosts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ai-engine/src/stream.ts`:
- Around line 63-71: The current chat_auth_request event type (type:
'chat_auth_request') makes failureMessage optional even when status is 'failed';
change the type into a discriminated union on the status field so that when
status === 'failed' failureMessage is required and when status is 'pending' |
'completed' failureMessage is absent/optional. Update the definition in
packages/ai-engine/src/stream.ts (the object with type: 'chat_auth_request',
messageId, mcpServerId, mcpServerLabel, authUrl, status, failureMessage) to
split into at least two interfaces/unions keyed by status and adjust any code
that references this event type to use the new union.
In `@packages/frontend/src/components/chat/auth-request-card.test.tsx`:
- Around line 30-37: The test currently creates a spy on window.open (openSpy)
and restores it inside the test body, which can leak if the test fails; change
the setup so window.open is spied in the test but its restoration is done in a
shared afterEach teardown: create the spy (vi.spyOn(window,
'open').mockImplementation(() => null)) in the test or a beforeEach, and move
the cleanup to an afterEach that calls openSpy.mockRestore() (or call
vi.restoreAllMocks()) to ensure the mock is always removed; reference the
openSpy/window.open usage in the test that renders AuthRequestCard and fires the
click.
In `@packages/frontend/src/components/chat/auth-request-card.tsx`:
- Around line 55-62: The message embeds block.mcpServerId in free text which
lets the model pick the wrong tool; change the send path so mcpServerId is sent
as a structured field/action instead of natural language: update the call site
that invokes sendChatMessage (in auth-request-card.tsx) to include a dedicated
payload or action property (e.g., { type: "complete_authentication",
mcpServerId: block.mcpServerId, url: trimmed, label: block.mcpServerLabel }) so
the agent receives an explicit mcpServerId the chat runner
(packages/ai-engine/src/chat-runner.ts) can match to pending blocks rather than
parsing it from the message body.
- Around line 15-27: The isLikelyCallbackUrl function currently accepts URLs
that include credentials (e.g. http://user:pass@localhost:54801/...), so update
isLikelyCallbackUrl to reject any URL with non-empty URL.username or
URL.password by checking u.username === '' && u.password === '' before returning
true; keep the existing protocol, host, and searchParams checks (function name:
isLikelyCallbackUrl) so credentialed callback URLs are not considered valid and
thus won't reach sendChatMessage.
In `@packages/frontend/src/lib/store.ts`:
- Around line 414-439: The code currently updates the first matching pending
auth_request by iterating messages/blocks forwards; change the traversal so you
locate and update the latest pending one by scanning messages and each
message.blocks from the end toward the start (i.e., reverse order) and stop
after the first match. In the implementation around updatedMessages/messages.map
and the inner m.blocks.map (and the b checks for b.type==='auth_request' &&
b.mcpServerId===evt.mcpServerId && b.status==='pending'), iterate blocks in
reverse (or find lastIndex) to set status/failureMessage on that single latest
block, then rebuild the blocks array preserving original order and mark
messageUpdated so you replace only the affected message.
---
Nitpick comments:
In `@packages/frontend/src/components/chat/auth-request-card.test.tsx`:
- Around line 68-77: Test suite for AuthRequestCard lacks a positive case
verifying IPv6 loopback (::1) is accepted; add a unit test in
packages/frontend/src/components/chat/auth-request-card.test.tsx that mirrors
the existing negative case but sets the input value to an IPv6 loopback callback
(e.g. "http://[::1]:3000/callback?code=AAA&state=xyz"), ensure
useCanvasStore.setState({ sendChatMessage: vi.fn() } as never) and
render(<AuthRequestCard block={pendingBlock} />) as in the other test, then
assert the submit button (screen.getByRole('button', { name: /^認証完了$/ })) is
enabled (disabled === false) to confirm AuthRequestCard allows ::1 hosts.
In `@packages/frontend/src/lib/store.ts`:
- Around line 385-470: The chat_auth_request handling is too large in
store.ts—extract it into a dedicated helper (e.g. applyAuthRequestEvent) or a
small module that receives (evt, currentMessages) and returns updated messages;
move the logic that inspects evt.status ('pending' | 'completed' | 'failed'),
searches/updates auth_request blocks by mcpServerId/status, appends failed
entries by evt.messageId when no pending exists, and preserves existing block
fields (mcpServerLabel, authUrl, failureMessage) into that helper; update the
store code to call get().chatThreadMessages, pass them and evt to
applyAuthRequestEvent, then call set({ chatThreadMessages: result }) and keep
the public behavior identical (use the same symbols: evt, chatThreadMessages,
blocks, mcpServerId, status, failureMessage).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 21cf645b-ec7b-4be0-947a-6cc95adf8dca
📒 Files selected for processing (17)
packages/ai-engine/src/agent-runner.test.tspackages/ai-engine/src/auth-detector.test.tspackages/ai-engine/src/auth-detector.tspackages/ai-engine/src/chat-runner.test.tspackages/ai-engine/src/chat-runner.tspackages/ai-engine/src/mcp/build-mcp-servers.test.tspackages/ai-engine/src/mcp/build-mcp-servers.tspackages/ai-engine/src/stream.tspackages/core/src/schema.test.tspackages/core/src/schema.tspackages/frontend/src/app/api/projects/[id]/route.test.tspackages/frontend/src/components/chat/auth-request-card.test.tsxpackages/frontend/src/components/chat/auth-request-card.tsxpackages/frontend/src/components/chat/chat-message.tsxpackages/frontend/src/components/dialog/project-settings-dialog.test.tsxpackages/frontend/src/components/dialog/project-settings-dialog.tsxpackages/frontend/src/lib/store.ts
CodeRabbit (PR #21) Major 4 件 + Minor 1 件への対応。 [Heavy lift / Major #4] OAuth callback を構造化 WS メッセージで送る これまで callback URL paste 後に「[OAuth callback for ${mcpServerId}] ...」 という自然文を sendChatMessage で送り、AI に mcpServerId を解釈させて mcp__<id>__complete_authentication を呼ばせていた。複数 MCP server を 同時運用する状況で AI が別 server の complete_authentication を選び、 元の auth_request カードが pending のまま残るリスクがあった。 UI から WS の構造化メッセージ {type:'oauth_callback', mcpServerId, callbackUrl} を送り、サーバ側で確定的に「指定 server の complete_authentication を呼べ」とプロンプトを生成する設計に変更。 - ai-engine/server.ts: ChatOAuthCallbackSchema 追加、handler 分岐 - ai-engine/chat-runner.ts: runOAuthCallback メソッド追加 (runUserTurn ラッパー) - frontend/lib/ws.ts: sendOAuthCallback 追加 - frontend/lib/store.ts: sendOAuthCallback action 追加 - frontend/auth-request-card.tsx: sendChatMessage → sendOAuthCallback - 関連 test 7 件を新 API に追従 [Quick win / Major #1] chat_auth_request を discriminated union 化 ai-engine/stream.ts: status='failed' のときのみ failureMessage を必須に する型分割。schema.ts (AuthRequestBlockSchema) の superRefine と型レベル で整合させ、永続化時の検証エラーを型チェックで早期検出。chat-runner の emit 側と test の type narrowing も追従。 [Quick win / Major #3] callback URL の username/password 拒否 auth-request-card.tsx: isLikelyCallbackUrl で URL 内資格情報 (user:pass@) を弾く。schema.ts (McpServerConfigSchema.url) と整合。 [Quick win / Major #5] 最新の pending auth_request を更新 frontend/lib/store.ts: chat_auth_request reducer の走査を末尾起点に変更。 再認証で複数 pending が並ぶケースで、古いカードを更新して新しいカードが pending のまま残る問題を解消。 [Quick win / Minor #2] window.open spy の cleanup を afterEach に auth-request-card.test.tsx: vi.restoreAllMocks() を afterEach に集約し、 テスト失敗時に spy が漏れて後続に影響する経路を塞ぐ。 [見送り / Minor (codex)] auth-detector.ts の id 命名規則 ADR codex セカンドオピニオン Minor m2 (regex / schema 二重メンテの ADR メモ) は 本 PR スコープ外として見送り。
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ai-engine/src/chat-runner.ts`:
- Around line 373-383: runOAuthCallback currently delegates to runUserTurn which
persists the raw callback text to history and allows normal tool/model flow;
instead implement an ephemeral OAuth-only execution path: replace the yield*
this.runUserTurn(text, []) call in runOAuthCallback with logic that emits the
user-facing event(s) without saving to conversation history and then invokes
only the whitelist tool named `mcp__${mcpServerId}__complete_authentication`
(reject/ignore any other tool/model actions). Use or add a helper such as an
ephemeral runner (e.g., runEphemeralTurn/runOAuthTurn) or inline the behavior to
mark messages as non-persistent and constrain tool execution to that single tool
name so callback `code`/`state` are never stored and no other MCP server
completions can run.
In `@packages/ai-engine/src/server.ts`:
- Around line 51-55: ChatOAuthCallbackSchema's callbackUrl currently uses
z.string().url() which is too permissive; update the schema to validate
callbackUrl more strictly to match the front-end rules: ensure the URL scheme is
either http or https, reject URLs with username/password (no credentials),
restrict the host to loopback addresses (localhost, 127.0.0.1, ::1 / [::1]), and
require that the query string contains both code and state parameters. Implement
this by replacing the simple .url() check on callbackUrl in
ChatOAuthCallbackSchema with a custom zod refinement or parsed-URL validator
that parses the URL, checks the protocol, hostname, absence of auth, and
presence of code and state query params while preserving the existing type
shape.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6dd10985-750c-46f9-a792-7177e3288074
📒 Files selected for processing (8)
packages/ai-engine/src/chat-runner.test.tspackages/ai-engine/src/chat-runner.tspackages/ai-engine/src/server.tspackages/ai-engine/src/stream.tspackages/frontend/src/components/chat/auth-request-card.test.tsxpackages/frontend/src/components/chat/auth-request-card.tsxpackages/frontend/src/lib/store.tspackages/frontend/src/lib/ws.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/ai-engine/src/stream.ts
CodeRabbit (PR #21) 4 周目 Major 2 件対応。 [Major #1 / Heavy lift] runOAuthCallback の ephemeral 化 これまで runOAuthCallback は \`yield* this.runUserTurn(text, [])\` を 呼んでおり、callback URL (code/state 含む) が user message として永続化 され、後続の prompt 再構築でも再送される問題があった。さらに通常 turn の ままなので、AI が他のツール (create_node 等) を実行する余地が残っていた。 専用の ephemeral 実行経路に切替: - chatStore.appendMessage を呼ばない (callback URL を chat 履歴に残さない) - assistantMsgId は ephemeral (handleAuthToolResult の orphan 失敗 fallback 以外では参照されない) - 対象 server の config のみ buildMcpServers に渡す - allowedTools = [\`mcp__<id>__complete_authentication\`] の 1 件のみ - tool_use は kind === 'complete_authentication' && id 一致のみ stash - text block と stash 不一致の tool_result は emit せず黙って捨てる - handleAuthToolResult が過去 message の最新 pending を更新する経路は維持 [Major #2] サーバ側 callbackUrl 検証強化 ChatOAuthCallbackSchema.callbackUrl が \`z.string().url()\` のみで、 loopback / no-credentials / code+state 必須を確認していなかった。 WS は UI 経由でない外部クライアントからも到達しうるので、isValidCallbackUrl ヘルパで isLikelyCallbackUrl (UI 側) と同じ条件をサーバ側でも検証する。
…保持) (PR-C、stacked on #21) (#22) * refactor(chat): ChatRunner を long-lived Query 化 + codex 指摘 4 件対応 PR-C の主機能: 1 ChatRunner = 1 sdk.query() = 1 subprocess に固定して、 MCP HTTP transport の OAuth 状態 (PKCE / token) を turn 跨ぎで保持する。 main 最新 (PR-A の MCP 統合 + PR-B の OAuth 2.1 / auth_request UX + 私の修正多数: XML escape / externalToolUseIds / ephemeral runOAuthCallback / 空 assistant プレースホルダ等) と統合した形で再実装。 ## 新規ファイル - packages/ai-engine/src/async-input.ts: SDK の streaming input mode 用 AsyncIterable<SdkUserMessage> 実装。push 可能 + close 即時 teardown (CR Major 反映: close 後 next() 即 done / FIFO waiter キュー) - packages/ai-engine/src/async-input.test.ts: 8 件 (push / waiter / close / FIFO / close 後 即 done) ## agent-runner.ts - SdkLike.query を `prompt: string | AsyncIterable<SdkUserMessageLike>` に拡張 - SdkUserMessageLike + SdkQueryHandle (任意 close()) 型追加 ## chat-runner.ts 新規フィールド: - query / input / outputLoopDone / outputLoopFailed / currentTurn / cachedExternalConfigById - ensureQueryInflight (Promise キャッシュ、再入ガード) 新規 interface: - TurnState: 1 user turn の間だけ生きる mutable state (assistantMsgId / queue / textBuffer / stashedAuthUses / externalConfigById / externalToolUseIds) 新規メソッド: - ensureQuery: 1 度だけ query 立ち上げ + 出力ループ起動。inflight Promise キャッシュで再入ガード (codex Major 3) - startQueryInternal: 内部の起動本体 - runOutputLoop: SDK message を currentTurn の queue に振り分け - dispatchSdkMessage: 1 メッセージ処理 (text/tool_use/tool_result/result) - tearDownQuery / close: リソース cleanup。close は outputLoopDone を退避 してから tearDownQuery を呼ぶ (codex 致命 Minor: close 順序) 挙動変更: - runUserTurn: 入口で currentTurn ガード (codex Major 1: turn 並走防止) - input null チェックを invariant assertion で表明 (codex Major 2) - 空 assistant message 永続化を ensureQuery 前に移動 (long-lived では bg loop が即起動するため、後置きだと race で空 message 残る) - ephemeral runOAuthCallback も long-lived query 経由に統合 (callback URL は input.push 経由で SDK に渡るが chatStore には 永続化されない) main 最新の機能を保持: - auth-detector / handleAuthToolResult / stashedAuthUses - externalToolUseIds (TurnState に統合) - escapeXmlText / escapeXmlAttr (buildChatPrompt) - 空 assistant プレースホルダ「(認証処理を完了しました)」 (dispatchSdkMessage の result 処理に統合) buildMcpServer は 1 引数化、handler 内で this.currentTurn から assistantMsgId / emit を動的解決 (1 度作った MCP サーバを turn 跨ぎ で使い回せる、turn 中は不変)。 ## server.ts - WS close 時に runner.close() を呼ぶ。close 内部の reject は .catch で warn (codex Major: void close() で unhandled rejection 化を回避) ## chat-runner.test.ts - startCapturePromptText helper を追加: prompt が AsyncIterable<...> に変わったので、最初に push された user message の content を 奪取する (string にも互換) - 既存 test の prompt キャプチャ 4 箇所を helper 経由に書き換え ## テスト pnpm typecheck: 4/4 PASS pnpm test: core 93 + storage 88 + ai-engine 213 + frontend 273 = 667 PASS pnpm lint: exit 0 * fix(chat): CR 5 周目 Major 1 + Minor 1 対応 (Major 1 は見送り) CodeRabbit (PR #22) 1 周目への対応。 [Minor / chat-runner.ts:246] ensureQuery 失敗時の空 assistant ロールバック long-lived Query 化に伴い空 assistant message を ensureQuery 前に append するよう変更したが、ensureQuery 失敗時に空バブルが履歴に残る問題があった。 chatStore に message 単位の delete API が無いため、catch 内で replaceMessageBlocks でエラー内容を blocks に書き込む形でロールバックする。 [Major / chat-runner.ts:361] 正常 EOF と subprocess 終了の区別 runOutputLoop の正常 EOF 経路では従来 chat_turn_ended のみを emit していたが、 これだと「予期しない subprocess 終了」も「明示 shutdown」も区別できず、 途中で切れた turn が正常完了に見えてしまう問題があった。 ChatRunner.isClosing フラグを追加し、close() 内で立てる。runOutputLoop の EOF ブランチで isClosing が false なら agent_failed event を先に emit してから chat_turn_ended で turn を閉じる。 [Major / chat-runner.ts:599] OAuth callback の long-lived 経路統合 (見送り) callback URL を this.input に push することで会話 context に turn 跨ぎで残る、allowedTools 単一制約が prompt 依存になる、という懸念。 PR-C の主目的 (OAuth state を turn 跨ぎで保持) と SDK 制約のトレードオフ。 ephemeral 化すると subprocess 分離で state が引き継がれず再認証が必要になる。 本 PR では long-lived 維持を選択し、prompt 内で「他 tool は呼ぶな」と明示する 形でモデル依存の防御を入れる。SDK の MCP transport が状態共有 API を提供する までは追加対応見送り。CR thread に詳細を返信。 * fix(chat): CR 2 周目 Major 4 件対応 (Quick win) CodeRabbit (PR #22) 2 周目への対応。Heavy lift #3 (OAuth callback の ephemeral 化) は前 PR と同様 long-lived 維持で見送り、CR thread に返信。 [Major / chat-runner.ts:349] close と ensureQuery の競合 close() が startQueryInternal の途中 (await projectStore.getProjectMeta() 等) で走ると、shutdown 後に sdk.query() が作られて subprocess が孤立する race があった。close() で ensureQueryInflight を join してから tearDown する。 [Major / chat-runner.ts:358] SDK メッセージの常時ログによる機密漏洩 runOutputLoop の console.log が全 SDK メッセージを redactMcpSecrets でラップ してログしていたが、message.content や result の OAuth callback URL の code/state がサーバーログに残るリスクがあった。type だけ出す形に絞り、 本文は出さない。redactMcpSecrets の import も削除。 [Major / chat-runner.ts:386] 異常終了後の pendingApprovals 残存 runOutputLoop の catch / EOF ブランチで queue を閉じるだけだったので、 turn 失敗後に UI から approveTool() が来ると create_node / create_edge 等 の side effect が走る恐れがあった。rejectAllPendingApprovals helper を 新設し、両ブランチで pendingApprovals を一括 reject(false) する。 [Major / chat-runner.ts:625] runOAuthCallback の currentTurn 解放保証 this.currentTurn = turnState の直後から全体を try/finally で囲み、 appendMessage 等の中間ステップで throw しても currentTurn が解放される ことを保証 (turn_in_progress で次の turn を受け付けられなくなるのを防ぐ)。 runUserTurn 側も同じパターンで全体を try/finally に統一。
Summary
PR #18 を 4 PR に割り直したうちの OAuth 2.1 ピボット + auth_request UX 部分。base = #19 (PR-A) にスタック。
PR-A の PAT 認証前提を撤回し、MCP プロトコル OAuth 2.1 / SDK 任せの実装に切替える。同時に、SDK の `mcp____authenticate` tool_use を生のまま並べると UX が破綻する (URL がプレーンテキスト + redirect 先 localhost:XXXXX が即死) 問題を、専用 `auth_request` ブロック + `AuthRequestCard` で 1 等地化する。
変更内容
`3f7e63f` refactor: OAuth 2.1 採用 / PAT 排除
Premise 9 撤回:
`1f8c676` feat(chat): auth_request UX
`9e2679d` test: route.test.ts PATCH assert
事前登録 PATCH の status を assert (CR Minor 指摘)。
テスト
```
pnpm typecheck # 4/4 PASS
pnpm test # 91 + 88 + 209 + 272 = 660 PASS
```
残課題
次 turn で再 auth が必要」が残る (SDK subprocess が turn ごとに再生成され
OAuth state が引き継がれない)。これは PR-C (ChatRunner long-lived 化) で対応。
Stack
Test plan
Summary by CodeRabbit
New Features
Chores
Bug Fixes